﻿using System;
using System.Collections.Generic;
using System.IO;

namespace Zquence_Studio_Port.Conversions
{
    internal class MidiFormat
    {
        // Standard MIDI file header, for one-track file
        // 4D, 54... are just magic numbers to identify the
        //  headers
        // Note that because we're only writing one track, we
        //  can for simplicity combine the file and track headers
        int[] header = new int[]
        {
            0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06,	// MThd
            0x00, 0x01, // multi-track format
            0x00, 0x01, // one track
            0x00, 0x10, // 16 ticks per quarter
        };

        int[] track = new int[]
        {
            0x4d, 0x54, 0x72, 0x6B // MTrk
        };

        // Standard footer
        int[] footer = new int[]
        {
            0x01, 0xFF, 0x2F, 0x00
        };

        // A MIDI event to set the tempo
        int[] tempoEvent = new int[]
        {
            0x00, 0xFF, 0x51, 0x03,
            0x0F, 0x42, 0x40 // Default 1 million usec per crotchet
        };

        // A MIDI event to set the key signature. This is irrelent to
        //  playback, but necessary for editing applications 
        int[] keySigEvent = new int[]
        {
            0x00, 0xFF, 0x59, 0x02,
            0x00, // C
            0x00  // major
        };


        // A MIDI event to set the time signature. This is irrelent to
        //  playback, but necessary for editing applications 
        int[] timeSigEvent = new int[]
        {
            0x00, 0xFF, 0x58, 0x04,
            0x04, // numerator
            0x02, // denominator (2==4, because it's a power of 2)
            0x30, // ticks per click (not used)
            0x08  // 32nd notes per crotchet 
        };

        private int mBpm;
        private int mDivision;

        public List<MidiFormatTrackData> TrackDataEvents;

        //------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Construct a new MidiFile with an empty playback event list.
        /// </summary>
        public MidiFormat()
        {
            TrackDataEvents = new List<MidiFormatTrackData>();
        }

        //------------------------------------------------------------------------------------------------------------------------
        public void SetTempo(int value)
        {
            byte[] data = ConvertIntToByteArray(value, 3, true);

            tempoEvent[6] = data[0];
            tempoEvent[5] = data[1];
            tempoEvent[4] = data[2];
        }

        //------------------------------------------------------------------------------------------------------------------------
        public void SetBPM(double bpm)
        {
            int tempo = (int)(500000 * (120 / bpm));
            SetTempo(tempo);
        }

        public int BPM
        {
            get { return mBpm; }
            set
            {
                mBpm = value;
                int tempo = (int)(500000 * (120 / value));
                SetTempo(tempo);
            }
        }

        public int Division
        {
            get { return mDivision; }
            set
            {
                if (value > ushort.MaxValue)
                {
                    throw new Exception("Division support up to " + ushort.MaxValue);
                }

                mDivision = value;
                header[13] = value;
                header[12] = value >> 8;
            }
        }

        //------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Write the stored MIDI events to a file.
        /// </summary>
        public void WriteToFile(Stream stream)
        {
            //RandomAccessFile fos = new RandomAccessFile(filename);

            byte[] tracks = ConvertIntToByteArray(TrackDataEvents.Count, 2, false);
            header[11] = tracks[1];
            header[10] = tracks[0];

            byte[] arr = intArrayToByteArray(header);
            stream.Write(arr, 0, arr.Length);

            WriteTracks(stream);
        }

        //------------------------------------------------------------------------------------------------------------------------
        public void WriteTracks(Stream fos)
        {
            try
            {
                int a = -1;
                foreach (MidiFormatTrackData td in TrackDataEvents)
                {
                    byte[] arr = null;

                    a++;
                    arr = intArrayToByteArray(track);
                    fos.Write(arr, 0, arr.Length);

                    // Calculate the amount of track data
                    // _Do_ include the footer but _do not_ include the 
                    // track header

                    int size = tempoEvent.Length + keySigEvent.Length + timeSigEvent.Length + footer.Length + td.TrackNameData.Length;

                    for (int i = 0; i < td.playEvents.Count; i++)
                    {
                        size += td.playEvents[i].Length;
                    }

                    // Write out the track data size in big-endian format
                    // Note that this math is only valid for up to 64k of data
                    //  (but that's a lot of notes) 
                    int high = size / 256;
                    int low = size - (high * 256);
                    fos.WriteByte((byte)0);
                    fos.WriteByte((byte)0);
                    fos.WriteByte((byte)high);
                    fos.WriteByte((byte)low);

                    // Track name
                    arr = intArrayToByteArray(td.TrackNameData);
                    fos.Write(arr, 0, arr.Length);

                    // Write the standard metadata — tempo, etc
                    // At present, tempo is stuck at crotchet=60 
                    arr = intArrayToByteArray(tempoEvent);
                    fos.Write(arr, 0, arr.Length);
                    arr = intArrayToByteArray(keySigEvent);
                    fos.Write(arr, 0, arr.Length);
                    arr = intArrayToByteArray(timeSigEvent);
                    fos.Write(arr, 0, arr.Length);

                    // Write out the note, etc., events
                    for (int i = 0; i < td.playEvents.Count; i++)
                    {
                        //System.Windows.Forms.MessageBox.Show("Track: " + a + " Note: " + i);
                        byte[] send = intArrayToByteArray(td.playEvents[i]);
                        fos.Write(send, 0, send.Length);
                    }

                    // Write the footer and close
                    arr = intArrayToByteArray(td.TrackNameData);
                    fos.Write(arr, 0, arr.Length);
                }
            }
            catch (Exception e)
            {

            }
        }

        //------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Convert an array of integers which are assumed to contain
        /// unsigned bytes into an array of bytes.
        /// </summary>
        private static byte[] intArrayToByteArray(int[] ints)
        {
            int l = ints.Length;
            byte[] out_ = new byte[ints.Length];
            for (int i = 0; i < l; i++)
            {
                out_[i] = (byte)ints[i];
            }
            return out_;
        }

        private static byte[] ConvertIntToByteArray(int value, int length, bool invert)
        {
            byte[] b = new byte[length];

            if (invert)
            {
                for (int i = 0; i < length; i++)
                {
                    int offset = (b.Length - 1 - i) * 8;
                    b[length - i - 1] = (byte)((value >> offset) & 0xFF); //???
                }
            }
            else
            {
                for (int i = 0; i < length; i++)
                {
                    int offset = (b.Length - 1 - i) * 8;
                    b[i] = (byte)((value >> offset) & 0xFF);//???
                }
            }
            return b;
        }
    }
}
